SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
6.2 KB · 115 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { AdminTitle, Bool, JsonPre, Mono, StatusChip } from '@/components/admin/ui';5import { TierBadge } from '@/components/ui/badges';6import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';7import { KeyValue } from '@/components/ui/key-value';8import { LiveAgo } from '@/components/ui/live';9import { Unavailable } from '@/components/ui/unavailable';10import { adminApi, load, requireAdmin } from '@/lib/admin/admin-api';11import type { AdminSnapshotRow } from '@/lib/admin/types';12import { fmtBytes, fmtDateTime, fmtInt, num } from '@/lib/format';13import { routes } from '@/lib/site';1415export const metadata: Metadata = { title: 'Document', robots: { index: false, follow: false } };16export const dynamic = 'force-dynamic';1718export default async function AdminDocumentPage({ params }: { params: Promise<{ id: string }> }) {19  await requireAdmin();20  const { id } = await params;21  const res = await load(adminApi.document(id));22  if (!res.ok && res.error.startsWith('404')) notFound();23  if (!res.ok) {24    return (25      <>26        <AdminTitle title="Document" />27        <Unavailable what="Document" reason={res.error} />28      </>29    );30  }31  const d = res.data;32  const snaps: AdminSnapshotRow[] = Array.isArray(d.snapshots) ? d.snapshots : [];33  const meta = { ...(d.meta ?? {}) } as Record<string, unknown>;34  delete meta.raw_path;35  delete meta.text_path;36  return (37    <>38      <p className="mb-2 text-xs"><Link href="/admin/documents" className="link">← Documents</Link></p>39      <AdminTitle title={d.title ?? d.url.replace(/^https?:\/\/(www\.)?/, '')} lede={<Mono>{d.id}</Mono>} />40      <div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_20rem]">41        <div className="min-w-0">42          <KeyValue43            dense44            rows={[45              { key: 'url', label: 'URL', value: <a href={d.url} target="_blank" rel="noopener noreferrer" className="link break-all">{d.url}</a> },46              { key: 'canonical_url', label: 'Canonical', raw: d.canonical_url },47              { key: 'connector', label: 'Connector', value: <Link href={`/admin/documents?connector=${encodeURIComponent(d.connector_name ?? '')}`} className="mono text-xs hover:text-accent">{d.connector_name ?? '—'}</Link> },48              { key: 'source', label: 'Source', value: <span className="text-sm">{d.source_name ?? d.source_id ?? '—'} {d.source_tier !== undefined && d.source_tier !== null && <TierBadge tier={num(d.source_tier)} />}</span> },49              { key: 'doc_type', label: 'Type', raw: d.doc_type },50              { key: 'entity', label: 'Entity', value: d.entity_slug && d.entity_type ? <Link href={routes.entity({ entity_type: d.entity_type, slug: d.entity_slug })} className="link">{d.entity_name ?? d.entity_slug}</Link> : <span className="text-ink-3">—</span> },51              { key: 'status', label: 'Status', value: <StatusChip value={d.status} /> },52              { key: 'processing', label: 'Processing', value: <StatusChip value={d.last_processing_status ?? null} /> },53              { key: 'first_seen_at', label: 'First seen', raw: d.first_seen_at ? fmtDateTime(d.first_seen_at) : null },54              { key: 'last_fetched_at', label: 'Last fetched', raw: d.last_fetched_at ? fmtDateTime(d.last_fetched_at) : null },55              { key: 'last_changed_at', label: 'Last changed', raw: d.last_changed_at ? fmtDateTime(d.last_changed_at) : null },56              { key: 'next_fetch_at', label: 'Next fetch', raw: d.next_fetch_at ? fmtDateTime(d.next_fetch_at) : null },57              { key: 'counts', label: 'Fetch / change / fail', value: <span className="tnum">{fmtInt(d.fetch_count)} / {fmtInt(d.change_count)} / {fmtInt(d.fail_count)}</span> },58              { key: 'last_status', label: 'Last HTTP', raw: d.last_status },59              { key: 'etag', label: 'ETag', value: d.etag ? <Mono>{d.etag}</Mono> : undefined },60              { key: 'content_hash', label: 'Content hash', value: d.content_hash ? <Mono className="break-all">{d.content_hash}</Mono> : undefined },61              { key: 'priority', label: 'Priority', raw: d.priority },62              { key: 'needs_llm', label: 'Needs LLM', value: <Bool v={d.needs_llm} /> },63            ]}64          />65        </div>66        <aside className="min-w-0">67          <p className="eyebrow mb-2">Meta</p>68          <JsonPre value={Object.keys(meta).length ? meta : null} maxHeight="20rem" />69        </aside>70      </div>7172      <section className="mt-8">73        <p className="eyebrow mb-2">Snapshots <span className="tnum text-ink-3">{snaps.length}</span></p>74        <DataTable compact scroll caption="Snapshots">75          <thead>76            <tr>77              <Th>Observed</Th>78              <Th num>HTTP</Th>79              <Th num>Bytes</Th>80              <Th>Changed</Th>81              <Th>Processing</Th>82              <Th>Parser</Th>83              <Th>Transport</Th>84              <Th>Structured</Th>85              <Th>Text</Th>86              <Th>Run</Th>87            </tr>88          </thead>89          <tbody>90            {snaps.length === 0 && <EmptyRow cols={10}>No snapshots archived for this document.</EmptyRow>}91            {snaps.map((s) => (92              <tr key={s.id}>93                <Td>94                  <Link href={`/admin/snapshots/${encodeURIComponent(s.id)}`} className="text-xs text-ink hover:text-accent" title={s.id}>95                    {fmtDateTime(s.observed_at)} · <LiveAgo at={s.observed_at} />96                  </Link>97                </Td>98                <Td num className={`tnum text-xs ${(num(s.http_status) ?? 0) >= 400 ? 'text-danger' : ''}`}>{fmtInt(s.http_status)}</Td>99                <Td num className="tnum text-xs">{fmtBytes(s.byte_size)}</Td>100                <Td><Bool v={s.changed} /></Td>101                <Td><StatusChip value={s.processing_status} /></Td>102                <Td><Mono>v{s.parser_version ?? '?'}</Mono></Td>103                <Td><Mono>{s.transport ?? '—'}</Mono></Td>104                <Td><Bool v={s.has_structured} /></Td>105                <Td><Bool v={s.has_text} /></Td>106                <Td>{s.run_id ? <Mono>{s.run_id}</Mono> : <span className="text-ink-3">—</span>}</Td>107              </tr>108            ))}109          </tbody>110        </DataTable>111      </section>112    </>113  );114}115